<!--
 * @Description: 
 * @Author: HuRrongChao
 * @Date: 2025-01-16 11:39:07
 * @LastEditors: HuRrongChao
 * @LastEditTime: 2025-02-24 00:08:54
-->
<template>
  <div class="barrage-container" ref="containerRef" :style="{ width: containerSize.width + 'px', height: containerSize.height + 'px' }">
    <slot v-for="(item, index) in showBarrages" :key="index" name="barrage" :value="item">
      <div
        :class="['barrage', item.paused ? 'active' : '']"
        @mousemove.stop="() => {
          item.paused = true
        }"
        @mouseleave.stop="() => {
          item.paused = false
        }"
        :style="{
          top: `${item.y}px`,
          animation: `${item.name} 1s linear forwards paused`,
          animationDelay: `-${item.x / (containerSize.width + item.width)}s`,
          [`margin${upperCase(props.direction)}`]: `-${item.width}px`,
          [direction]: 0, fontSize: item.fontSize + 'px', color: item.color, padding: '5px 20px'}"
      >{{ item.value }}</div>
    </slot>
  </div>
</template>

<script setup>
import { ref, onBeforeUnmount, watch } from 'vue'
import { raf, cancelAnimationFrame, timeSlice, getWidth, upperCase } from './utils'

// data为一个对象，要传入value，fontSize，color，出现时间time，标识key, 可传入speed速度--每个渲染帧帧运动的距离
const props = defineProps({
  data: {
    type: Array,
    default: () => []
  },
  videoDom: {
    type: [String, Element],
    default: ''
  },
  direction: {
    type: String,
    default: 'right'
  }
})

const containerRef = ref() // 容器
const containerSize = ref({ // 容器大小
  width: 0,
  height: 0
})
const videoDom = ref() // 视频dom
const showBarrages = ref([]) // 渲染在页面上的弹幕数量
const stashBarrages = ref([]) // 暂存的弹幕
const barragesRows = ref([]) // 弹幕弹道, 每一项的索引代表轨道，每一项存着该轨道的弹幕
const isPlay = ref(false) // 是否播放弹幕
const isFilter = ref(false) // 是否过滤弹幕
const isInit = ref(false) // 是否初始化

// 暂存的弹幕数量
const showLength = () => {
  return showBarrages.value.length
}

watch(() => props.data, () => {
  isInit.value = false
})

// 初始化
const init = () => {
  stashBarrages.value = props.data
  // 排序
  stashBarrages.value.sort((a, b) => a.time - b.time)
  if (typeof(props.videoDom) === 'boolean') {
    videoDom.value = document.getElementById(props.videoDom)
  } else {
    videoDom.value = props.videoDom
  }
  containerSize.value.width = videoDom.value.clientWidth
  containerSize.value.height = videoDom.value.clientHeight
  isInit.value = true
}

// 初始化弹幕
const initBarrage = () => {
  if (stashBarrages.value.length > 0) {
    let length = 50 - showLength()
    // 获取当前视频的时间
    const time = videoDom.value.currentTime

    if (length > 0) {
      timeSlice(length, () => {
        // 初始化一个弹幕
        if (stashBarrages.value[0] && stashBarrages.value[0].time <= time) {
          const currentBarrage = stashBarrages.value[0]
          // 弹幕宽度
          const { width, height} = getWidth({
            value: currentBarrage.value,
            style: {
              fontSize: currentBarrage.fontSize + 'px',
              padding: '5px 20px'
            }
          })
          if (!currentBarrage.width) currentBarrage.width = width
          if (!currentBarrage.height) currentBarrage.height = height
          // 弹道
          if (barragesRows.value.length === 0) {
            const length = parseInt(containerSize.value.height / height)
            // 用fill是因为new Array生成的数组只有长度属性，没有具体每个项，不能遍历
            // 若用forEach，在这个回调函数中，it 是值，将 it 设置为 []，但这不会影响原始数组。所以用map
            barragesRows.value = new Array(length).fill(0).map(() => [])
          }
          const index = getRandomIndex(currentBarrage, 0)
          if (!index) return // 没有轨道，返回
          currentBarrage.rowIndex = index
          barragesRows.value[index].push(currentBarrage)
          // 动画
          const name = 'move' + currentBarrage.key
          currentBarrage.name = name
          const isNegative = props.direction === 'left' ? 1 : -1
          const sheet = document.styleSheets[0]
          const keyframes = `@keyframes ${name} { 0% { transform: translateX(0); } 100% { transform: translateX(${(containerSize.value.width + width) * isNegative}px); } }`
          sheet.insertRule(keyframes, sheet.cssRules.length)
          // 初始化位置
          setPos(currentBarrage)
          currentBarrage.y = index * height
          // console.log('index', index,'currentBarrage.y', currentBarrage.y,'currentBarrage.value', currentBarrage.value, 'currentBarrage.x', currentBarrage.x, 'currentBarrage.width', currentBarrage.width)
          showBarrages.value.push(currentBarrage)
          stashBarrages.value.shift()
        }
      })
    }
  }
}

// 随机取一个轨道
const getRandomIndex = (currentBarrage, num = 0) => {
  if (num >= barragesRows.value.length) return // 所有轨道都遍历了
  const randomIndex = Math.floor(Math.random() * barragesRows.value.length)
  // 进行碰撞检测，由于 css 动画的误差导致追尾的时间计算会有一点点的误差
  const computingDuration = (prevBarrage, currentBarrage) => { // 计算追尾的时间
    if (prevBarrage.width > prevBarrage.x) { // 前一个还未移动完成
      // console.log('前一个还未移动完成', prevBarrage.value)
      return null
    }
    const prevWidth = prevBarrage.width
    const prevSpeed = prevBarrage.speed
    const currentSpeed = currentBarrage.speed || 0
    const acceleration = currentSpeed - prevSpeed
    
    // 如果速度差小于等于 0，永远不可能追上
    if (acceleration <= 0) {
      return randomIndex
    }

    const distance = prevBarrage.x - prevWidth  // 当前弹幕相遇时比前一个多走的距离，即当前弹幕出发时，前一个已走的距离。 因为是头尾相遇，故要减去prevWidth 和
    const meetTime = distance / acceleration
  
    // 如果相遇时间大于于当前弹幕的运动时间，则肯会在容器视图外面追尾，不用管
    if (meetTime >= ((currentBarrage.width + containerSize.width) / currentBarrage.speed)) {
      return randomIndex
    }

    return null
  
    // 把此次弹幕运动时间修改为上一个弹幕移除屏幕的时间，这样追尾的情况在刚刚移除视图的时候进行
    // const remainingTime = (1 - prevBarrage.getMovePercent()) * prevBarrage.duration
    // const currentFixTime = currentWidth * remainingTime / this.containerWidth

    // return remainingTime + currentFixTime
  }
  const curRow = barragesRows.value[randomIndex]
  if (curRow.length === 0) return randomIndex // 如果该轨道没其他弹幕，直接返回
  const preBarrage = curRow[curRow.length - 1]
  const index = computingDuration(preBarrage, currentBarrage)
  if (index) {
    return index
  }
  num++
  return getRandomIndex(currentBarrage, num) 
}

// 设置位置
const setPos = (currentBarrage) => {
  if (!currentBarrage.x) currentBarrage.x = 0
  if (!currentBarrage.speed) currentBarrage.speed = 1
  // 溢出容器
  if (currentBarrage.x > containerSize.value.width + currentBarrage.width) {
    const showIndex = showBarrages.value.findIndex(it => it.key === currentBarrage.key)
    showBarrages.value.splice(showIndex, 1)
    const rIndex = barragesRows.value[currentBarrage.rowIndex].findIndex(it => it.key === currentBarrage.key)
    barragesRows.value[currentBarrage.rowIndex].splice(rIndex, 1)
    // 删除规则
    const sheet = document.styleSheets[0]
    let cssIndex = null
    for (let i = 0; i < sheet.cssRules.length; i++) {
      if (sheet.cssRules[i].name && sheet.cssRules[i].name === currentBarrage.name) {
        cssIndex = i
        break
      }
    }
    if (cssIndex !== null) sheet.deleteRule(cssIndex)
  }
  // 增加距离
  if (!currentBarrage.paused) currentBarrage.x = currentBarrage.x + currentBarrage.speed
}

// 不断移动
let rafId = null
const startMove = () => {
  if (isPlay.value) {
    !isInit.value && init()
    isFilter.value && filterBarrage()
    initBarrage()
    console.log('渲染')
    showBarrages.value.forEach(item => setPos(item))
    rafId = raf(startMove)
  }
}

// 过滤弹幕
const filterBarrage = () => {
  const time = videoDom.value.currentTime
  
  // 从后向前遍历，否则当您删除一个元素后，数组的长度会减少，导致下一个元素的索引会向前移动，从而可能导致某些元素未被检查。
  for (let i = stashBarrages.value.length - 1; i >= 0; i--) {
    if (stashBarrages.value[i].time < time) {
      stashBarrages.value.splice(i, 1)
    }
  }

  isFilter.value = false
}

// 开启过滤
const openFilter = () => {
  isFilter.value = true
}

// 弹幕播放
const playBarrage = () => {
  isPlay.value = true
  startMove()
}

// 弹幕暂停
const pauseBarrage = () => {
  cleanupAnimation()
}

// 清空弹幕
const clearBarrage = () => {
  showBarrages.value = []
  stashBarrages.value = []
  barragesRows.value = []
  cleanupAnimation()
}

// 添加清理函数
const cleanupAnimation = () => {
  if (rafId) {
    cancelAnimationFrame(rafId)  // 取消动画帧
    rafId = null
  }
  isPlay.value = false
}

// 添加弹幕
const addBarrage = (item) => {
  props.data.push(item)
  isInit.value = false
}

// 在组件销毁前清理
onBeforeUnmount(() => {
  clearBarrage()
})

defineExpose({
  playBarrage,
  pauseBarrage,
  clearBarrage,
  openFilter,
  addBarrage,
  init
})
</script>

<style scoped>
.barrage-container {
  position: absolute;
  top: 0;
  left: 0;
}

.barrage {
  position: absolute;
  display: inline-block;
}

.active {
  border-radius: 15px;
  background: rgba(0,0,0,0.4);
}
</style>